package luz.javaproxy.proxy;


import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

import luz.javaproxy.common.Tuple;
import luz.javaproxy.plugin.PluginInterface;
import luz.javaproxy.plugin.PluginMgr;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.RequestLine;
import org.apache.http.impl.NoConnectionReuseStrategy;
import org.apache.http.impl.nio.DefaultClientIOEventDispatch;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.nio.params.NIOReactorPNames;
import org.apache.http.nio.protocol.ThrottlingHttpClientHandler;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.http.nio.reactor.IOReactorException;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;


class HttpProxy implements HttpRequestHandler {
	private ConnectingIOReactor httpProxy;
	private static final Log log = LogFactory.getLog(HttpProxy.class);
	final HttpParams httpProxyParams;
	private PluginMgr mgr;
	
	public HttpProxy(PluginMgr mgr) {
		super();
		
		this.mgr=mgr;
		
		httpProxyParams = new BasicHttpParams();
        httpProxyParams
            .setIntParameter(		CoreConnectionPNames.SO_TIMEOUT				, 60000)
            .setIntParameter(		CoreConnectionPNames.CONNECTION_TIMEOUT		, 60000)
            .setIntParameter(		CoreConnectionPNames.SOCKET_BUFFER_SIZE		, 20 * 1024)
            .setIntParameter(       NIOReactorPNames.CONTENT_BUFFER_SIZE        , 20 * 1024)
            .setBooleanParameter(	CoreConnectionPNames.STALE_CONNECTION_CHECK	, true)
            .setBooleanParameter(	CoreConnectionPNames.TCP_NODELAY			, true);
        
        
        BasicHttpProcessor httpProxyProcessor = new BasicHttpProcessor();
             
        final ThrottlingHttpClientHandler httpProxyHandler = new ThrottlingHttpClientHandler(
        		httpProxyProcessor, 
        		new HttpProxyExecutionHandler(), 
        		new NoConnectionReuseStrategy(), 
        		Executors.newFixedThreadPool(3),
        		httpProxyParams);
        httpProxyHandler.setEventListener(new HttpProxyEventLogger());
        
//        final BufferingHttpClientHandler httpProxyHandler = new BufferingHttpClientHandler(
//                httpProxyProcessor,
//                new HttpProxyExecutionHandler(),
//                new DefaultConnectionReuseStrategy(),
//                httpProxyParams);
//        httpProxyHandler.setEventListener(new HttpProxyEventLogger());
        
        try {
			httpProxy = new DefaultConnectingIOReactor(1, httpProxyParams);
		} catch (IOReactorException e) {
			log.error("Could not create IOReactor",e);
		}

        new Thread(new Runnable() {
            public void run() {
                try {
                	httpProxy.execute(new DefaultClientIOEventDispatch(httpProxyHandler, httpProxyParams));
                } catch (InterruptedIOException ex) {	log.error("Interrupted");
                } catch (IOException e) {				log.error("I/O error: " + e.getMessage());
                }
                log.warn("Shutdown");
            }
            
        }).start();
	}

	public void handle(final HttpRequest request, final HttpResponse response, final HttpContext context) throws HttpException, IOException {
		//////////////////Request//////////////////////////////////
		log.info("Request : "+request.getRequestLine());
		String serverHost = request.getFirstHeader("host").getValue();
		boolean proxy=false;

		mgr.onRequest(request);
		String proxyAddr=(String)request.getParams().getParameter(PluginInterface.PROXY);
		if (proxyAddr!=null){
			serverHost=proxyAddr;
			proxy=true;
		}
		

		String address;
		int port;
		int splitter=serverHost.indexOf(':');
		if (splitter==-1){
			address=serverHost;	
			port=80;
		}else{
			address=serverHost.substring(0, splitter);
			port=Integer.parseInt(serverHost.substring(splitter+1, serverHost.length()));
		}
		

		log.info("Using address: "+address+ " port: "+port);	

		InetSocketAddress serverSocketAddr = new InetSocketAddress(address, port);
		InetAddress  serverInetaddress = serverSocketAddr.getAddress();
		if (serverInetaddress==null){
			log.warn("serverHost unresolvable: "+serverHost);
			response.setStatusCode(404);
			mgr.onResponse(request, null);
			return;
		}
		
		
		Object trigger = new Object();
		Tuple<Object, Object> container = new Tuple<Object, Object>(trigger, proxyDuties(request, proxy));	//late request change. a plugin might have changed it.

		log.info("Creating new connection to: "+serverHost);
		log.info("Resulting Request: "+request.getRequestLine());
		httpProxy.connect(serverSocketAddr, null, container, null);

		log.info("Session started, waiting for response");
        synchronized (trigger) {
            try {
				trigger.wait(30*1000);	//blocking
			} catch (InterruptedException e) {
				log.warn("Interrupted", e);
				return;
			}
			if(container.getSecond()==null){
				log.warn("Request timed out");
				return;
			}
		}
        log.info("Session waiting done. Got resopnse");
		
		//////////////////Response//////////////////////////////////
		HttpResponse serverResponse = (HttpResponse)container.getSecond();
		
		//Header
        int statusCode=serverResponse.getStatusLine().getStatusCode();
        log.info("Response: "+serverResponse.getStatusLine());
		response.setStatusCode(statusCode);
		for (Header header : serverResponse.getAllHeaders())
			response.setHeader(header);

		//Body
		response.setEntity(serverResponse.getEntity());	//Normal case

		mgr.onResponse(request, response);
		
		
		//Clean-up ???
		if (request instanceof HttpEntityEnclosingRequest) {
		    HttpEntity entity = ((HttpEntityEnclosingRequest)request).getEntity();
		    if (entity != null)
		        entity.consumeContent();
		}
	}
	
	private HttpRequest proxyDuties(HttpRequest request,  boolean proxy){
//		for (Header header : request.getAllHeaders()) System.err.println(header);

		/*
		 * RFC2616 14.10
		 * The Connection general-header field ... MUST NOT be communicated by proxies over further connections.
		 * for each connection-token in this field, remove any header field(s) from the message with the same name as the connection-token
		 */
		Header[] conn=request.getHeaders("Connection");
		String[] connectionTokens;
		for (Header header : conn){
			connectionTokens=header.getValue().split(",");
			for (String connectionToken : connectionTokens)
				request.removeHeaders(connectionToken.trim());	//connection-token
			request.removeHeader(header);						//Connection general-header 
		}

		/*
		 * Non-RFC Proxy-Connection header (similar to Connection)
		 */
		conn=request.getHeaders("Proxy-Connection");
		for (Header header : conn){
			connectionTokens=header.getValue().split(",");
			for (String connectionToken : connectionTokens)
				request.removeHeaders(connectionToken.trim());
			request.removeHeader(header);
		}
		
		/*
		 * RFC2616 14.33
		 * the Proxy-Authenticate header field applies only to the current connection
		 * and SHOULD NOT be passed on to downstream clients
		 */
		request.removeHeaders("Proxy-Authenticate");
		
		/*
		 * HTTP/1.1 applications that do not support persistent connections
		 *  MUST include the "close" connection option in every message. 
		 *  
		 *  c.f. NoConnectionReuseStrategy()
		 */
		request.addHeader("Connection", "close");
		
		//remove the full address from the request again (added by the browser for proxies)
		//for target servers but not for proxies
		/*
		 * RFC2616 1.4
		 * A proxy is a forwarding agent, receiving requests for a URI in its absolute form
		 */
		if (!proxy){
			String host = request.getFirstHeader("Host").getValue();
			String uri = request.getRequestLine().getUri();
			int pos = uri.indexOf(host);
			if (pos!=-1)
				setUri(request.getRequestLine(), uri.substring(pos+host.length()));
		}
		return request;
	}
	
	public void shutdown(){
		try {
			httpProxy.shutdown();
		} catch (IOException e) {
			log.warn("Shutdown error", e);
		}
	}
	
	private void setUri(RequestLine requestLine, String newUri){
		try {
			Field field = requestLine.getClass().getDeclaredField("uri");
			field.setAccessible(true);
			field.set(requestLine, newUri);
		} catch (SecurityException e)        {log.warn("setUri: "+requestLine, e);
		} catch (NoSuchFieldException e)     {log.warn("setUri: "+requestLine, e);
		} catch (IllegalArgumentException e) {log.warn("setUri: "+requestLine, e);
		} catch (IllegalAccessException e)   {log.warn("setUri: "+requestLine, e);
		}
	}


}